package com.kw.common.feign.ext;

import cn.hutool.core.util.StrUtil;
import com.alibaba.cloud.sentinel.feign.SentinelContractHolder;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.context.ContextUtil;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kw.common.core.util.R;
import com.kw.common.core.util.SpringContextHolder;
import feign.*;
import feign.hystrix.FallbackFactory;
import io.seata.core.context.RootContext;
import io.seata.core.exception.TransactionException;
import io.seata.tm.api.GlobalTransactionContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;

import static feign.Util.checkNotNull;


@Slf4j
public class SentinelInvocationHandler implements InvocationHandler {

	public static final String EQUALS = "equals";

	public static final String HASH_CODE = "hashCode";

	public static final String TO_STRING = "toString";

	private final Target<?> target;

	private final Map<Method, InvocationHandlerFactory.MethodHandler> dispatch;

	private FallbackFactory<?> fallbackFactory;

	private Map<Method, Method> fallbackMethodMap;

	public SentinelInvocationHandler(Target<?> target, Map<Method, InvocationHandlerFactory.MethodHandler> dispatch,
									 FallbackFactory<?> fallbackFactory) {
		this.target = checkNotNull(target, "target");
		this.dispatch = checkNotNull(dispatch, "dispatch");
		this.fallbackFactory = fallbackFactory;
		this.fallbackMethodMap = toFallbackMethod(dispatch);
	}

	public SentinelInvocationHandler(Target<?> target, Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) {
		this.target = checkNotNull(target, "target");
		this.dispatch = checkNotNull(dispatch, "dispatch");
	}

	@Override
	public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
		ObjectMapper objectMapper = SpringContextHolder.getBean(ObjectMapper.class);
		if (EQUALS.equals(method.getName())) {
			try {
				Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
				return equals(otherHandler);
			}
			catch (IllegalArgumentException e) {
				return false;
			}
		}
		else if (HASH_CODE.equals(method.getName())) {
			return hashCode();
		}
		else if (TO_STRING.equals(method.getName())) {
			return toString();
		}


		Object result;
		InvocationHandlerFactory.MethodHandler methodHandler = this.dispatch.get(method);
	//	ObjectMapper objectMapper = SpringContextHolder.getBean(ObjectMapper.class);
		// only handle by HardCodedTarget

		if (target instanceof Target.HardCodedTarget) {
			Target.HardCodedTarget<?> hardCodedTarget = (Target.HardCodedTarget) target;
			MethodMetadata methodMetadata = SentinelContractHolder.METADATA_MAP
					.get(hardCodedTarget.type().getName() + Feign.configKey(hardCodedTarget.type(), method));
			// resource default is HttpMethod:protocol://url
			if (methodMetadata == null) {
				result = methodHandler.invoke(args);
			}

			else {
				String resourceName = methodMetadata.template().method().toUpperCase() + ':' + hardCodedTarget.url()
						+ methodMetadata.template().path();
				Entry entry = null;
				try {
					ContextUtil.enter(resourceName);
					entry = SphU.entry(resourceName, EntryType.OUT, 1, args);
					result = methodHandler.invoke(args);
				}
				catch (Throwable ex) {

					//执行回滚
					rollBackTransAction(resourceName);


					// fallback handle
					if (!BlockException.isBlockException(ex)) {
						Tracer.trace(ex);
					}

					if (fallbackFactory != null) {
						try {
							//根据fallbackFactoryMap 找到fackback 并且invoke
							return fallbackMethodMap.get(method).invoke(fallbackFactory.create(ex), args);
						}
						catch (IllegalAccessException e) {
							// shouldn't happen as method is public due to being an
							// interface
							throw new AssertionError(e);
						}
						catch (InvocationTargetException e) {
							throw new AssertionError(e.getCause());
						}
					}
					//如果不为null 则没有配置fallback策略
					else {
						if (R.class == method.getReturnType()) {
							ex.printStackTrace();
							if(ex instanceof FeignException){
								FeignException exception = (FeignException) ex;
								byte[] content = exception.content();
								String str = StrUtil.str(content, StandardCharsets.UTF_8);
								log.error("feign FeignException 异常:message:[{}],资源:[{}]",str,resourceName);
								return objectMapper.readValue(str, R.class);
							}else{
								log.error("feign异常:{} ,资源:{}",ex.getMessage(),resourceName);
								return R.failed(ex.getMessage());
							}
						}
						else {
							throw ex;
						}
					}
				}
				finally {

					if (entry != null) {
						entry.exit(1, args);
					}
					ContextUtil.exit();
				}
			}
		}
		else {
			// other target type using default strategy
			result = methodHandler.invoke(args);
		}

		return result;
	}

	private void rollBackTransAction(String resourceName) throws TransactionException {
		log.info("执行回滚开始==>"+resourceName +",XID==>"+RootContext.getXID());
		if (StringUtils.isNotBlank(RootContext.getXID())) {
			GlobalTransactionContext.reload(RootContext.getXID()).rollback();
		}
		log.info("执行回滚结束==>"+resourceName);
	}

	@Override
	public boolean equals(Object obj) {
		if (obj instanceof com.alibaba.cloud.sentinel.feign.SentinelInvocationHandler) {
			SentinelInvocationHandler other = (SentinelInvocationHandler) obj;
			return target.equals(other.target);
		}
		return false;
	}

	@Override
	public int hashCode() {
		return target.hashCode();
	}

	@Override
	public String toString() {
		return target.toString();
	}

	static Map<Method, Method> toFallbackMethod(Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) {
		Map<Method, Method> result = new LinkedHashMap<>();
		for (Method method : dispatch.keySet()) {
			method.setAccessible(true);
			result.put(method, method);
		}
		return result;
	}

}
